use cgmath::prelude::*;

pub struct Camera {
    pub pos: cgmath::Point3<f32>,
    pub up: cgmath::Vector3<f32>,
    pub yaw: cgmath::Deg<f32>,
    pub pitch: cgmath::Deg<f32>,

    pub speed: f32,
    pub rot_speed: cgmath::Deg<f32>,

    pub fovy: cgmath::Rad<f32>,
    pub aspect: f32,
    pub near: f32,
    pub far: f32
}

impl Camera {
    const OPENGL_TO_WGPU_MATRIX: cgmath::Matrix4<f32> = cgmath::Matrix4::new(
        1.0, 0.0, 0.0, 0.0,
        0.0, 1.0, 0.0, 0.0,
        0.0, 0.0, 0.5, 0.5,
        0.0, 0.0, 0.0, 1.0,
    );

    pub const CMD_FRONT    : u32 = 1 << 0;
    pub const CMD_BACK     : u32 = 1 << 1;
    pub const CMD_RIGHT    : u32 = 1 << 2;
    pub const CMD_LEFT     : u32 = 1 << 3;
    pub const CMD_UP       : u32 = 1 << 4;
    pub const CMD_DOWN     : u32 = 1 << 5;
    pub const CMD_ROT_RIGHT: u32 = 1 << 6;
    pub const CMD_ROT_LEFT : u32 = 1 << 7;
    pub const CMD_ROT_UP   : u32 = 1 << 8;
    pub const CMD_ROT_DOWN : u32 = 1 << 9;

    pub fn handle_cmds(&mut self, cmds: u32, time_span: f32) {
        let mov = self.speed * time_span;
        let rot = self.rot_speed * time_span;

        if cmds & Self::CMD_ROT_RIGHT != 0 { self.yaw += rot; }
        if cmds & Self::CMD_ROT_LEFT  != 0 { self.yaw -= rot; }
        if cmds & Self::CMD_ROT_UP    != 0 { self.pitch += rot; }
        if cmds & Self::CMD_ROT_DOWN  != 0 { self.pitch -= rot; }
        if self.pitch > cgmath::Deg(89.0) { self.pitch = cgmath::Deg(89.0); }
        if self.pitch < cgmath::Deg(-89.0) { self.pitch = cgmath::Deg(-89.0); }

        let front = cgmath::Vector3::new(self.yaw.cos(), 0.0, self.yaw.sin());

        if cmds & Self::CMD_FRONT != 0 { self.pos += mov * front; }
        if cmds & Self::CMD_BACK  != 0 { self.pos -= mov * front; }
        if cmds & Self::CMD_RIGHT != 0 { self.pos += mov * front.cross(self.up); }
        if cmds & Self::CMD_LEFT  != 0 { self.pos -= mov * front.cross(self.up); }
        if cmds & Self::CMD_UP    != 0 { self.pos += mov * self.up; }
        if cmds & Self::CMD_DOWN  != 0 { self.pos -= mov * self.up; }
    }

    pub fn get_view_proj_mat(&self) -> cgmath::Matrix4<f32> {
        let pitch_cos = self.pitch.cos();
        let facing = cgmath::Vector3::new(
            self.yaw.cos() * pitch_cos,
            self.pitch.sin(),
            self.yaw.sin() * pitch_cos
        );

        let view_mat = cgmath::Matrix4::look_to_rh(self.pos, facing, self.up);

        let proj_mat = cgmath::perspective(self.fovy, self.aspect, self.near, self.far);

        return Self::OPENGL_TO_WGPU_MATRIX * proj_mat * view_mat;
    }
}

#[repr(C)]
#[derive(Clone, Copy, bytemuck::Zeroable, bytemuck::Pod)]
pub struct CameraUniform {
    view_proj_mat: [[f32; 4]; 4]
}

impl CameraUniform {
    pub fn new(camera: &Camera) -> Self {
        Self {
            view_proj_mat: camera.get_view_proj_mat().into()
        }
    }

    pub const VISIBILITY: wgpu::ShaderStages = wgpu::ShaderStages::VERTEX;
}
